#!/bin/sh -efu
#
# Copyright (C) 2006-2018  Alexey Gladkov <legion@altlinux.org>
# Copyright (C) 2006-2012  Dmitry V. Levin <ldv@altlinux.org>
# Copyright (C) 2006  Sergey Vlasov <vsu@altlinux.org>
# Copyright (C) 2019  Vladimir D. Seleznev <vseleznv@altlinux.org>
#
# gear-update updates subdirectory from source directory or archive file.
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#

. gear-update-sh-functions

handlers_list=
show_help()
{
	local list
	list="'$(printf '%s\n' "$handlers_list" |
	         sed "s/\([[:alnum:]]\) \([[:alnum:]]\)/\1', '\2/g")';"
	cat <<EOF
$PROG - update subdirectory from source directory or archive file.

Usage: $PROG [Options] <source> <subdirectory>

Options:

  --exclude=PATTERN   exclude files matching posix-egrep regular expression PATTERN;
  --ignore-exclude    ignore .gitignore and .git/info/exclude (the default);
  --honor-exclude     honor .gitignore and .git/info/exclude;
  --remove-source     remove the <source> after updating;
  --strip-components=NUMBER
                      strip NUMBER of leading path components from file names;
  -a,--all            extract all files and directories from archive;
  -c,--create         create the <subdirectory> before unpacking the <source>,
                      the <subdirectory> should not exist;
  -s,--subdir=DIR     extract specified directory name from archive;
  -t,--type=TYPE      define source type:
                      $list
  -f,--force          remove files from <subdirectory> even
                      if untracked or modified files found;
  -v,--verbose        print a message for each action;
  -V,--version        print program version and exit;
  -h,--help           show this text and exit.

Report bugs to http://bugzilla.altlinux.org/

EOF
	exit
}

print_version()
{
	cat <<EOF
$PROG version $PROG_VERSION
Written by Alexey Gladkov <legion@altlinux.org>

Copyright (C) 2006-2018  Alexey Gladkov <legion@altlinux.org>
Copyright (C) 2006-2012  Dmitry V. Levin <ldv@altlinux.org>
Copyright (C) 2006  Sergey Vlasov <vsu@altlinux.org>
Copyright (C) 2019  Vladimir D. Seleznev <vseleznv@altlinux.org>
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
EOF
	exit
}

subdir=
source_handler=
validate_subdir()
{
	if [ -z "$subdir" ]; then
		subdir="$($source_handler --ls)"
		subdir="$(printf %s "$subdir" |
			  sed 's#\(\([./]\+\)\?[^/]\+\)/.*#\1#g' |
			  LC_ALL=C grep '^[^[:space:]]' |
			  LC_COLLATE=C sort -u)"
	fi

	[ -n "$subdir" ] ||
		fatal "$source: subdirectory not found."

	[ "`printf %s "$subdir" |wc -l`" -eq 0 ] ||
		fatal "More than one subdirectory specified: $(printf %s "$subdir" |tr -s '[:space:]' ' ')"

	local quoted_subdir
	quote_sed_regexp_variable quoted_subdir "$subdir"
	$source_handler --ls |LC_ALL=C grep -qs "^$quoted_subdir\(/\|\$\)" ||
		fatal "$source: directory \`$subdir' not found in archive."

	# We found the subdirectory automatically and we found this directory
	# in the archive so we need to update the variable.
	export gear_update_subdir="$subdir"
}

destdir=
dest_create=
cleanup_handler()
{
	if [ "$*" != 0 ]; then
		# Rollback actions
		if [ -d "$destdir" ]; then
			find "$destdir" -mindepth 1 -maxdepth 1 \! -name '.git' -print0 |
				xargs -r0 rm $verbose -rf --
		else
			rm $verbose -f -- "$destdir"
		fi

		if [ -z "$dest_create" ]; then
			mkdir -p -- "$destdir"
			cd "$destdir"
			git checkout-index -a
		fi
		message 'failed to update target directory.'
	fi
}

path_to_self="$(type -p "$0")" && [ -x "$path_to_self" ] ||
	fatal "Failed to locate \"$0\" binary"
[ -z "${path_to_self##/*}" ] ||
	path_to_self="$(readlink -ev "$path_to_self")"
plugin_dir="${path_to_self%/*}"
set +f
for s_handler in "$plugin_dir/gear-update-src-"*; do
	[ -f "$s_handler" ] ||
		break
	if [ ! -x "$s_handler" ]; then
		message "$s_handler: Handler must be executable."
		continue
	fi
	handlers_list="${handlers_list:+$handlers_list }${s_handler##*/gear-update-src-}"
done
set -f

TEMP=`getopt -n $PROG -o a,c,f,t:,s:,v,V,h -l all,create,exclude:,ignore-exclude,honor-exclude,force,remove-source,type:,subdir:,strip-components:,verbose,version,help -- "$@"` || show_usage
eval set -- "$TEMP"

gear_config_option exclude_pattern exclude-pattern ''
ignore_exclude=
honor_exclude=
force=
remove_source=
source_type=
all_subdirs=
strip_components=0
while :; do
	case "$1" in
	    -a|--all) all_subdirs=1
		;;
	    -c|--create) dest_create=1
		;;
	    --exclude) shift
		exclude_pattern="$1"
		;;
	    --ignore-exclude) ignore_exclude=1
		;;
	    --honor-exclude) honor_exclude=1
		;;
	    --remove-source) remove_source=1
		;;
	    --strip-components)
			strip_components=$(opt_check_number "$1" "$2")
			shift
		;;
	    -f|--force) force=-f
		;;
	    -t|--type) shift; source_type="$1"
		;;
	    -s|--subdir) shift; subdir="$1"
		;;
	    -v|--verbose) verbose=-v
		;;
	    -V|--version) print_version
		;;
	    -h|--help) show_help
		;;
	    --) shift; break
		;;
	    *) fatal "unrecognized option: $1"
		;;
	esac
	shift
done

[ "$#" -ge 2 ] ||
	show_usage 'Not enough arguments.'
[ "$#" -eq 2 ] ||
	show_usage 'Too many arguments.'

if [ -n "$ignore_exclude" ] && [ -n "$honor_exclude" ]; then
	fatal "--ingore-exclude and --honor-exclude are mutually exclusive"
fi

source="$(readlink -ev -- "$1")"; shift
if [ -n "$dest_create" ]; then
	destdir="$(readlink -fv -- "$1")"
	[ ! -e "$destdir" ] ||
		fatal "$destdir: Destination directory already exists."
else
	destdir="$(readlink -ev -- "$1")"
fi
shift
canon_destdir="$destdir"

[ "$source" != "$destdir" ] ||
	fatal "\`$source' and \`$destdir' are the same place."
[ -n "$dest_create" ] || [ -d "$destdir" ] ||
	fatal "$destdir: not a directory."

[ -z "$subdir" ] || [ -z "$all_subdirs" ] ||
	fatal "Cannot use --all and --subdir at the same time."

# Export options
export gear_update_all_subdirs="$all_subdirs"
export gear_update_canon_destdir="$canon_destdir"
export gear_update_destdir="$destdir"
export gear_update_force="$force"
export gear_update_source="$source"
export gear_update_subdir="$subdir"
export gear_update_verbose="$verbose"
export gear_update_strip_components="$strip_components"

if [ -z "$source_type" ]; then
	for s_type in $handlers_list; do
		rc=0
		"$plugin_dir/gear-update-src-$s_type" --check-type || rc=$?
		case "$rc" in
			0)
				source_type="$s_type"
				break
				;;
			1)	exit 1
				;;
			2)	continue
				;;
		esac
	done
fi
source_handler="$plugin_dir/gear-update-src-$source_type"
[ -x "$source_handler" ] ||
	fatal "$source: unrecognized source type: $source_type"

# Change to toplevel directory
chdir_to_toplevel
topdir="$(readlink -ev .)"

# Make destdir relative
destdir="$destdir/"
destdir="${destdir#$topdir/}"
[ "$destdir" = "${destdir#/}" ] ||
	fatal "$destdir: directory out of \`$topdir' tree."
[ -n "$destdir" ] &&
	destdir="${destdir%/}" ||
	destdir='.'

initial_commit=
git rev-parse --verify HEAD >/dev/null 2>&1 || initial_commit=1

if [ -z "$force$initial_commit" ]; then
	out="$(git diff-index --cached --name-only HEAD -- "$destdir")"
	[ -z "$out" ] ||
		fatal "$destdir: Changed files found in the index."

	out="$(git diff --name-only -- "$destdir" &&
	       git ls-files --directory --others --exclude-per-directory=.gitignore -- "$destdir")"
	[ -z "$out" ] ||
		fatal "$destdir: Untracked or modified files found."
fi

[ -n "$dest_create$initial_commit" ] ||
	git rm -n -r $force -- "$destdir" >/dev/null

[ -n "$all_subdirs" ] ||
	validate_subdir

# Trap for rollback
install_cleanup_handler cleanup_handler

# Remove $destdir
if [ -z "$dest_create" ]; then
	find "$destdir" -mindepth 1 -maxdepth 1 \! \( -name '.git' -o -name '.gear' -o -name '.gear-rules' -o -name '.gear-tags' \) -print0 |
		xargs -r0 rm $verbose -rf --
	[ "$destdir" = '.' ] ||
		rmdir $verbose -- "$destdir"
fi

# Copy new sources
$source_handler --unpack

[ -d "$destdir" ] ||
	fatal "$destdir: result of extraction is not a directory."

if [ -n "$exclude_pattern" ]; then
	cd "$destdir"
	find . -regextype 'posix-egrep' -regex "$exclude_pattern" -print0 |
			xargs -r0 rm $verbose -rf --
	cd - >/dev/null
fi

if [ -n "$(find "$destdir" -maxdepth 0 -empty)" ]; then
	echo '.gitattributes export-ignore' >"$destdir/export-ignore"
else
	export_ignore="$(mktemp -- "$destdir/$PROG.XXXXXXXX")"
	echo '.gitattributes export-ignore' >"$export_ignore"
	find "$destdir" -type d -empty -exec ln -- "$export_ignore" '{}/.gitattributes' ';'
	rm -- "$export_ignore"
fi

if [ -z "$honor_exclude" ]; then
	git ls-files -z -- "$destdir" |
		git update-index --remove --force-remove -z --stdin
	git ls-files -z --others --modified -- "$destdir" |
		git update-index --add ${verbose:+--verbose} -z --stdin
else
	[ -n "$dest_create$initial_commit" ] ||
		git rm -f -r --cached -- "$destdir" >/dev/null
	git add $verbose -- "$destdir"
fi

if [ -n "$remove_source" ]; then
	[ -n "${source##$topdir/*}" ] ||
		git ls-files -z -- "$source" |
			git update-index --remove --force-remove -z --stdin
	rm -rf -- "$source"
fi
